package org.bhf.json;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Public entry point for parsing JSON.
 */
public final class JSONParser
{
    Collection<ConversionError> messages    = new LinkedList<ConversionError>();
    JSONParserGenerator         generator   = new JSONParserGenerator();
    List<String>                context     = new ArrayList<String>( 16 );

    /**
     *
     * @return Property path context
     * @exclude
     */
    public List<String> getContext()
    {
        return context;
    }

    /**
     *
     * @return Property conversion error messages
     * @exclude
     */
    public Collection<ConversionError> getMessages()
    {
        return messages;
    }

    //
    // Object Types
    //

    /**
     * Convenience method to read a <code>File</code> into a <code>String</code> for parsing.
     *
     * @param file <code>File</code> to read from. Must not be <code>null</code>.
     * @param charSet Character set in which the file is encoded. Must not be <code>null</code>.
     * @return String contents
     * @throws IOException I/O Error
     */
    public static String    readFile( final File file, final String charSet )
        throws IOException
    {
        final InputStream   fin     = new FileInputStream( file );
        final Reader        reader  = new InputStreamReader( fin, charSet );

        try
        {
            return readReader( reader );
        }
        finally
        {
            reader.close();
        }
    }

    /**
     * Convenience method to read from a <code>Reader</code> into a <code>String</code> for parsing.
     *
     * @param reader <code>Reader</code> to read from. Must not be <code>null</code>.
     * @return String contents
     * @throws IOException I/O Error
     */
    public static String    readReader( final Reader reader )
        throws IOException
    {
        final char[]        b   = new char[8192];
        final StringBuffer  sb  = new StringBuffer();

        for( int l; (l = reader.read( b )) > 0; )
            sb.append( b, 0, l );

        return sb.toString();
    }

    /**
     * Parse JSON and overlay on an existing object. If the state cannot be overlaid, such as with
     * String, the object is replaced. The modified or new object is returned. Use this method if
     * the target object is not <code>null</code>.
     *
     * @param object Must not be <code>null</code> will be modified in place.
     * @param json JSON
     * @return The parsed object. This object may be <code>object</code> modified in place or a new
     *      object of the same type as <code>object</code>. Will not be <code>null</code>
     * @throws java.io.IOException Read error or value format error
     * @throws ValueFormatException One or more format conversions, such as converting a string to an integer, failed.
     *      Parsing and state overlay continued and the property was set to its original value. The exception contains
     *      a list of the failed format conversions with context information.
     * @throws SyntaxError Unrecoverable JSON syntax error.
     */
    public <T> T parseObject( final T object, final String json )
        throws IOException
    {
        assert object != null;

        return parseObject( object.getClass(), object, json );
    }

    private final static Map<Type,Method> parsers = new HashMap<Type,Method>();

    /**
     * Parse JSON and overlay on an existing object. If the state cannot be overlaid, such as with
     * String, the object is replaced. The modified or new object is returned. Use this method if
     * the target object may be <code>null</code>.
     *
     * @param type Must not be <code>null</code>. Root type.
     * @param object May be be <code>null</code> and may be modified in place of not <code>null</code>. If
     *      <code>null</code>, the parser will attempt to instantiate an object of type <code>type</code>. Thus,
     *      <code>type</code> must be concrete and have a default public constructor.
     * @param json JSON
     * @return The parsed object. This object may be <code>object</code> modified in place or a new
     *      object of the same type as <code>object</code>. Must not be <code>null</code>
     * @throws java.io.IOException Read error or value format error
     * @throws ValueFormatException One or more format conversions, such as converting a string to an integer, failed.
     *      Parsing and state overlay continued and the property was set to its original value. The exception contains
     *      a list of the failed format conversions with context information.
     * @throws SyntaxError Unrecoverable JSON syntax error.
     */
    @SuppressWarnings("unchecked")
    public <T> T parseObject( final Type type, final T object, final String json )
        throws IOException
    {
        final JSONTokenizer tokens = new JSONTokenizer( json );
        final Method parser;

        // Resolve parser. Note that parsers is just a cache and may be garbage collected
        synchronized( parsers )
        {
            Method p = parsers.get( type );
            if( p == null )
            {
                final String parserClassName = generator.getParser( type );
                p = generator.resolveParser( type, parserClassName );
                parsers.put( type, p );
            }
            parser = p;
        }

        if( parser != null )
        {
            messages.clear();
            context.clear();

            // All the built-ins are for immutable objects, so we have to replace rather than overlay
            try
            {
                final T result;

                if( object == null )
                {
                    if( Boolean.TYPE == type )
                        result = (T)parser.invoke( null, false, tokens, this );
                    else if( String.class == type )
                        result = (T)parser.invoke( null, "", tokens, this );
                    else if( Byte.TYPE == type )
                        result = (T)parser.invoke( null, (byte)0, tokens, this );
                    else if( Short.TYPE == type )
                        result = (T)parser.invoke( null, (short)0, tokens, this );
                    else if( Integer.TYPE == type || Long.TYPE == type )
                        result = (T)parser.invoke( null, 0, tokens, this );
                    else if( Float.TYPE == type || Double.TYPE == type )
                        result = (T)parser.invoke( null, 0, tokens, this );
                    else if( Date.class == type )
                        result = (T)parser.invoke( null, new Date(), tokens, this );
                    else
                        result = (T)parser.invoke( null, object, tokens, this );
                }
                else
                {
                    result = (T)parser.invoke( null, object, tokens, this );
                }

                if( messages.size() > 0 )
                    throw new ValueFormatException( messages );

                return result;
            }
            catch( final IllegalAccessException iae )
            {
                throw new RuntimeException( iae );
            }
            catch( final InvocationTargetException ite )
            {
                final Throwable t = ite.getCause();
                if( t instanceof IOException )
                    throw (IOException)t;
                if( t instanceof RuntimeException )
                    throw (RuntimeException)t;
                if( t instanceof Error )
                    throw (Error)t;
                throw new RuntimeException( t );
            }
        }
        else
        {
            throw new UnsupportedTypeException( type );
        }
    }
}
